package com.xuebang.admin.mapper;

import java.lang.reflect.Field;
import java.util.Date;
import java.util.List;

import javax.inject.Inject;
import javax.persistence.Id;
import javax.persistence.Table;

import org.apache.ibatis.jdbc.SQL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;

import com.xuebang.admin.dao.Finder;
import com.xuebang.admin.entity.Syslog;
import com.xuebang.admin.util.SpringApplicationContext;
import com.google.common.base.CaseFormat;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;

public class SQLProvider<T> {
	
	private Logger logger = LoggerFactory.getLogger(SQLProvider.class);
	
	@Inject
	Finder finder;
	
	@SuppressWarnings("serial")
	static class BadFieldException extends DataAccessException {

		public BadFieldException(String msg, Throwable cause) {
			super(msg, cause);
		}

	}

	static class Column {
		public final Field field;
		public final String fieldName;
		public final String columnName;
		public boolean id;

		public Column(Field field, String fieldName, boolean id) {
			this.field = field;
			this.fieldName = fieldName;
			this.columnName = CaseFormat.LOWER_CAMEL.to(
					CaseFormat.LOWER_UNDERSCORE, fieldName);
			this.id = id;
		}
	}

	static class Metadata<T> {
		public final String tableName;
		public final List<Column> columns;
		public final Column idColumn;

		public final String idCondition;

		public Metadata(Class<T> klass) {
			Table table = checkTable(klass);
			tableName = getTableName(klass, table);

			columns = toColumns(klass);
			Column c = null;
			for (Column column : columns) {
				if (column.id) {
					c = column;
				}
			}
			idColumn = c;
			idCondition = c.columnName + "=#{" + c.fieldName + "}";
		}

		public String calculateSets(T entity, boolean includeId) {
			List<String> list = Lists.newArrayList();

			for (Column c : columns) {
				if (!includeId && c.id) {
					continue;
				}
				if (isNull(entity, c.field)) {
					continue;
				}

				list.add(c.columnName + "=#{" + c.fieldName + "}");
			}

			return Joiner.on(",").join(list);
		}

		public String calculateColumns(T entity, boolean includeId) {
			List<String> list = Lists.newArrayList();

			for (Column c : columns) {
				if (!includeId && c.id) {
					continue;
				}
				if (isNull(entity, c.field)) {
					continue;
				}

				list.add(c.columnName);
			}

			return Joiner.on(",").join(list);
		}

		private boolean isNull(T entity, Field f) {
			try {
				return f.get(entity) == null;
			} catch (Exception e) {
				throw new BadFieldException("can't get value for "
						+ f.getName(), e);
			}
		}

		public String calculateValues(T entity, boolean includeId) {
			List<String> list = Lists.newArrayList();

			for (Column c : columns) {
				if (!includeId && c.id) {
					continue;
				}
				if (isNull(entity, c.field)) {
					continue;
				}
				list.add("#{" + c.fieldName + "}");
			}

			return Joiner.on(",").join(list);
		}

		private Table checkTable(Class<?> klass) {
			Table table = klass.getAnnotation(Table.class);
			if (table == null) {
				throw new IllegalArgumentException(
						"entity must be annotated as a Table");
			}
			return table;
		}

		private String getTableName(Class<?> klass, Table table) {
			String name = table.name();
			if (Strings.isNullOrEmpty(name)) {
				name = klass.getSimpleName();
			}
			return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, name);
		}

		private boolean isValidField(Field field) {
			return field.isAnnotationPresent(Id.class)
					|| field.isAnnotationPresent(javax.persistence.Column.class);
		}

		private List<Column> toColumns(Class<?> klass) {
			Field[] fields = klass.getDeclaredFields();

			List<Column> result = Lists.newArrayList();
			for (Field field : fields) {
				if (!isValidField(field)) {
					continue;
				}

				field.setAccessible(true);
				String name = field.getName();
				result.add(new Column(field, name, field
						.isAnnotationPresent(Id.class)));
			}

			return result;
		}
	}

	private volatile Metadata<T> metadata;

	public String insert(T entity) {
		if (!entity.getClass().equals(Syslog.class)) { // Don't log changes on syslog. 
			System.err.println("inserting..." + entity.toString());
			logger.info("inserting..." + entity.toString());
			// logger.debug("inserting..."+entity.toString());
			checkMetadata(entity);
			Syslog log = new Syslog();
			log.setOperatorId(1L);
			log.setOperatorName("InsertTBD");// get Operator Name and ID later @todo
			log.setOperatorTime(new Date());
			log.setRemark("Insert:" + entity.toString());
			this.logSyslog(log);
		}
		return new SQL()
				.INSERT_INTO(metadata.tableName)
				.VALUES(metadata.calculateColumns(entity, false),
						metadata.calculateValues(entity, false)).toString();
	}

	public String update(T entity) {
		checkMetadata(entity);
		logger.debug("updating..."+entity.toString());
		
		Syslog log = new Syslog();
		log.setOperatorId(2L);
		log.setOperatorName("UpdateTBD");//get Operator Name and ID later @todo
		log.setOperatorTime(new Date());
		log.setRemark("Update:"+entity.toString());
		this.logSyslog(log);
		
		return new SQL().UPDATE(metadata.tableName)
				.SET(metadata.calculateSets(entity, false))
				.WHERE(metadata.idCondition).toString();
	}

	public String delete(T entity) {
		checkMetadata(entity);
		
		logger.debug("deleting..."+entity.toString());
		Syslog log = new Syslog();
		log.setOperatorId(3L);
		log.setOperatorName("DeleteTBD");//get Operator Name and ID later @todo
		log.setOperatorTime(new Date());
		log.setRemark("Delete:"+entity.toString());
		this.logSyslog(log);
		
		return new SQL().DELETE_FROM(metadata.tableName)
				.WHERE(metadata.idCondition).toString();
	}

	@SuppressWarnings("unchecked")
	private void checkMetadata(T entity) {
		if (metadata == null) {
			metadata = new Metadata<T>((Class<T>) entity.getClass());
		}
	}
	
	private void logSyslog(Syslog log){
		String sql = "INSERT INTO SYSLOG (OPERATOR_ID, OPERATOR_NAME,OPERATOR_TIME, REMARK) VALUES (?,?,NOW(),?)";
		Object[] params = new Object[]{
			log.getOperatorId(),
			log.getOperatorName(),
			log.getRemark()
		};
		
		finder = (Finder) SpringApplicationContext.getBean("finder");
		finder.update(sql,params);
	}

	public static void main(String[] args) {
//		SQLProvider<Visitor> provider = new SQLProvider<Visitor>();
//		Visitor v = new Visitor();
//		System.out.println(provider.insert(v));
//		System.out.println(provider.update(v));
//		System.out.println(provider.delete(v));
	}
}
